起名字这个技术活,终于用Pytorch找到解决办法了!
上节课我们使用深度学习技术创造了一个会写嘻哈说唱词的 AI,这节课我们使用类似的方法,再创建一个 AI国际起名大师!
AI 起名大师专起外国名,你只需告诉他你想要个哪国的名字,AI 起名大师分分钟给你想出一大堆名字出来!
下面都是是用 AI 起名大师生成的名字,有了它你还会为了起一个好的外国名而发愁吗
Russian
Rovakov Uantov Shavakov
German
Gerren Ereng Rosher
Spanish
Salla Parer Allan
资源:
集智 AI 学园公众号回复“AI起名大师”,获取本节课的 Jupyter Notebook 文档!
准备数据
做深度学习的第一步是把数据准备好。像之前一样,我们先准备数据。
这次的数据是18个文本文件,每个文件以“国家名字”命名,其中存储了这个国家的不同人名。
在读取这些数据前,为了简化神经网络的输入参数规模,我们把各国各语言人名都转化成用26个英文字母来表示,下面就是转换的方法。
import glob
import unicodedata
import string
# all_letters 即课支持打印的字符+标点符号
all_letters = string.ascii_letters + " .,;'-"
# Plus EOS marker
n_letters = len(all_letters) + 1
EOS = n_letters - 1
def unicode_to_ascii(s):
return ''.join(
c for c in unicodedata.normalize('NFD', s)
if unicodedata.category(c) != 'Mn'
and c in all_letters
)
print(unicode_to_ascii("O'Néàl"))
O'Neal
可以看到 "O'Néàl" 被转化成了以普通ASCII字符表示的 O'Neal。
在上面的代码中,还要注意这么几个变量。
print('all_letters: ', all_letters)
print('n_letters: ', n_letters)
print('EOS: ', EOS)
all_letters: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ .,;'-
n_letters: 59
EOS: 58
其中 all_letters 包含了我们数据集中所有可能出现的字符,也就是“字符表”。n_letters 是字符表的长度,在本例中长度为59。EOS 的索引号为58,它在字符表中没有对应的字符,仅代表结束。
读取数据
准备好处理数据的方法,下面就可以放心的读取数据了。
我们建立一个列表 all_categories 用于存储所有的国家名字。
建立一个字典 category_lines,以读取的国名作为字典的索引,国名下存储对应国别的名字。
# 按行读取出文件中的名字,并返回包含所有名字的列表
def read_lines(filename):
lines = open(filename).read().strip().split('\n')
return [unicode_to_ascii(line) for line in lines]
# category_lines是一个字典
# 其中索引是国家名字,内容是从文件读取出的这个国家的所有名字
category_lines = {}
# all_categories是一个列表
# 其中包含了所有的国家名字
all_categories = []
# 循环所有文件
for filename in glob.glob('../data/names/*.txt'):
# 从文件名中切割出国家名字
category = filename.split('/')[-1].split('.')[0]
# 将国家名字添加到列表中
all_categories.append(category)
# 读取对应国别文件中所有的名字
lines = read_lines(filename)
# 将所有名字存储在字典中对应的国别下
category_lines[category] = lines
# 共有的国别数
n_categories = len(all_categories)
print('# categories: ', n_categories, all_categories)
print()
print('# names: ', category_lines['Russian'][:10])
# categories: 18 ['Arabic', 'Chinese', 'Czech', 'Dutch', 'English', 'French', 'German', 'Greek', 'Irish', 'Italian', 'Japanese', 'Korean', 'Polish', 'Portuguese', 'Russian', 'Scottish', 'Spanish', 'Vietnamese']
# names: ['Ababko', 'Abaev', 'Abagyan', 'Abaidulin', 'Abaidullin', 'Abaimoff', 'Abaimov', 'Abakeliya', 'Abakovsky', 'Abakshin']
现在我们的数据准备好了,可以搭建神经网络了!
搭建神经网络
这次使用的 RNN 神经网络整体结构上与之前相似,细节上增加了一部分内容。
在输入层增加了 category,即名字的国别。这个 category 是以独热编码向量(one-hot vector)的方式输入的,再啰嗦一遍就是,作为长度为18的向量,代表自己国别的位置为1, 其它位置都为0。
我们搭建的神经网络的目的是生成“名字”,而“名字”就是一序列的字符。所以神经网络的输出(output)代表的是字符表中的每个字符,能成为“下一个字符”的概率,概率最大的那个字符,即作为“下一个字符”。
另外,这次还加入了“第二层”神经网络,即 o2o 层,以增强神经网络的预测性能。在 o2o 中还包括一层“dropout”,它会将输入“softmax”之前的数据随机置0(在本例中为随机置0.1)。“dropout”常被用来缓解“过拟合(overfitting)”的问题,因为它可以增加“混乱(chaos)”并提高采样的多样性。
建立神经网络的代码比较简单,并且网络的各个部分都在上图中标识出来了。
import torch
import torch.nn as nn
from torch.autograd import Variable
class RNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(RNN, self).__init__()
self.input_size = input_size
self.hidden_size = hidden_size
self.output_size = output_size
self.i2h = nn.Linear(n_categories + input_size + hidden_size, hidden_size)
self.i2o = nn.Linear(n_categories + input_size + hidden_size, output_size)
self.o2o = nn.Linear(hidden_size + output_size, output_size)
self.softmax = nn.LogSoftmax()
def forward(self, category, input, hidden):
input_combined = torch.cat((category, input, hidden), 1)
hidden = self.i2h(input_combined)
output = self.i2o(input_combined)
output_combined = torch.cat((hidden, output), 1)
output = self.o2o(output_combined)
return output, hidden
def init_hidden(self):
return Variable(torch.zeros(1, self.hidden_size))
准备训练
首先建立一个可以随机选择数据对 (category, line) 的方法,以方便训练时调用。
import random
def random_training_pair():
# 随机选择一个国别名
category = random.choice(all_categories)
# 读取这个国别名下的所有人名
line = random.choice(category_lines[category])
return category, line
对于训练过程中的每一步,或者说对于训练数据中每个名字的每个字符来说,神经网络的输入是 (category, current letter, hidden state),输出是 (next letter, next hidden state)。所以在每 批次 的训练中,我们都需要“一个国别”、“对应国别的一批名字”、“以及要预测的下一个字符”。
与上节课一样,神经网络还是依据“当前的字符”预测“下一个字符”。比如对于“Kasparov”这个名字,创建的数据对是 ("K", "a"), ("a", "s"), ("s", "p"), ("p", "a"), ("a", "r"), ("r", "o"), ("o", "v"), ("v", "EOS")。
国别名(category)也是以独热编码向量的形式输入神经网络的,它是一个 <1 x n_categories> 的向量。在输入的时候我们将它“并”在字符向量里,再“并”上隐藏层状态,作为神经网络的输入。
# 将名字所属的国家名转化为“独热向量”
def make_category_input(category):
li = all_categories.index(category)
tensor = torch.zeros(1, n_categories)
tensor[0][li] = 1
return Variable(tensor)
# 将一个名字转化成矩阵Tensor
# 矩阵的每行为名字中每个字符的独热编码向量
def make_chars_input(chars):
tensor = torch.zeros(len(chars), n_letters)
# 遍历每个名字中的每个字符
for ci in range(len(chars)):
char = chars[ci]
# 独热编码
tensor[ci][all_letters.find(char)] = 1
# 增加一个维度
tensor = tensor.view(-1, 1, n_letters)
return Variable(tensor)
# 将“目标”,也就是“下一个字符”转化为Tensor
# 注意这里最后以 EOS 作为结束标志
def make_target(line):
# 从第2个字符开始,取出每个字符的索引
letter_indexes = [all_letters.find(line[li]) for li in range(1, len(line))]
# 在最后加上 EOS 的索引
letter_indexes.append(n_letters - 1) # EOS
# 转化成 LongTensor
tensor = torch.LongTensor(letter_indexes)
return Variable(tensor)
同样为了训练时方便使用,我们建立一个 random_training_set 函数,以随机选择出数据集 (category, line) 并转化成训练需要的 Tensor: (category, input, target)。
def random_training_set():
# 随机选择数据集
category, line = random_training_pair()
# 转化成对应 Tensor
category_input = make_category_input(category)
line_input = make_chars_input(line)
line_target = make_target(line)
return category_input, line_input, line_target
开始训练!
与之前处理得分类问题不同,在分类问题中只有最后的输出被使用。而在当前的 生成 名字的任务中,神经网络在每一步都会做预测,所以我们需要在每一步计算损失值。
PyTorch 非常易用,它允许我们只是简单的把每一步计算的损失加起来,并在最后进行反向传播。
def train(category_tensor, input_line_tensor, target_line_tensor):
hidden = rnn.init_hidden()
optimizer.zero_grad()
loss = 0
for i in range(input_line_tensor.size()[0]):
output, hidden = rnn(category_tensor, input_line_tensor[i], hidden)
loss += criterion(output, target_line_tensor[i])
loss.backward()
optimizer.step()
return output, loss.data[0] / input_line_tensor.size()[0]
我们定义 time_since 函数,它可以打印出训练持续的时间。
import time
import math
def time_since(t):
now = time.time()
s = now - t
m = math.floor(s / 60)
s -= m * 60
return '%dm %ds' % (m, s)
训练的过程与我们前几节课一样,就是调用训练函数,再等上几分钟就好啦吼吼!
通过 plot_every 控制打印日志的频率,通过 all_losses 控制记录绘图数据的频率,已经都是老套路啦!
n_epochs = 100000
print_every = 5000
plot_every = 500
all_losses = []
loss_avg = 0 # Zero every plot_every epochs to keep a running average
learning_rate = 0.0005
rnn = RNN(n_letters, 128, n_letters)
optimizer = torch.optim.Adam(rnn.parameters(), lr=learning_rate)
criterion = nn.CrossEntropyLoss()
start = time.time()
for epoch in range(1, n_epochs + 1):
output, loss = train(*random_training_set())
loss_avg += loss
if epoch % print_every == 0:
print('%s (%d %d%%) %.4f' % (time_since(start), epoch, epoch / n_epochs * 100, loss))
if epoch % plot_every == 0:
all_losses.append(loss_avg / plot_every)
loss_avg = 0
0m 20s (5000 5%) 1.7640
0m 43s (10000 10%) 2.0930
1m 9s (15000 15%) 2.0813
1m 32s (20000 20%) 2.3847
1m 53s (25000 25%) 2.3773
2m 16s (30000 30%) 1.0473
2m 38s (35000 35%) 2.4157
2m 59s (40000 40%) 1.4604
3m 21s (45000 45%) 3.6522
3m 43s (50000 50%) 1.4112
4m 5s (55000 55%) 1.9039
4m 24s (60000 60%) 2.0797
4m 44s (65000 65%) 2.6220
5m 3s (70000 70%) 1.7693
5m 22s (75000 75%) 1.4734
5m 41s (80000 80%) 1.1522
6m 1s (85000 85%) 2.6431
6m 22s (90000 90%) 1.7703
6m 41s (95000 95%) 1.9069
7m 0s (100000 100%) 2.2563
绘制观察损失曲线
让我们将训练过程中记录的损失绘制成一条曲线,观察下神经网络学习的效果。
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
%matplotlib inline
plt.figure()
plt.plot(all_losses)
[<matplotlib.lines.Line2D at 0x114e8ec50>]
正如所有的优秀的神经网络一样^_^,损失值逐步降低并稳定在一个范围中。
测试使用神经网络
既然神经网络训练好了,那也就是说,我们喂给它第一个字符,他就能生成第二个字符,喂给它第二个字符,它就会生成第三个,这样一直持续下去,直至生成 EOS 才结束。
那下面我们编写 generate_one 函数以方便的使用神经网络生成我们想要的名字字符串,在这个函数里我们定义以下内容:
建立输入国别,开始字符,初始隐藏层状态的 Tensor
创建 output_str 变量,创建时其中只包含“开始字符”
定义生成名字的长度最大不超过 max_length
将当前字符传入神经网络
在输出中选出预测的概率最大的下一个字符,同时取出当前的隐藏层状态
如果字符是 EOS,则生成结束
如果是常规字符,则加入到 output_str 中并继续下一个流程
返回最终生成的名字字符串
max_length = 20
# 通过指定国别名 category
# 以及开始字符 start_char
# 还有混乱度 temperature 来生成一个名字
def generate_one(category, start_char='A', temperature=0.5):
category_input = make_category_input(category)
chars_input = make_chars_input(start_char)
hidden = rnn.init_hidden()
output_str = start_char
for i in range(max_length):
output, hidden = rnn(category_input, chars_input[0], hidden)
# 这里是将输出转化为一个多项式分布
output_dist = output.data.view(-1).div(temperature).exp()
# 从而可以根据混乱度 temperature 来选择下一个字符
# 混乱度低,则趋向于选择网络预测最大概率的那个字符
# 混乱度高,则趋向于随机选择字符
top_i = torch.multinomial(output_dist, 1)[0]
# 生成字符是 EOS,则生成结束
if top_i == EOS:
break
else:
char = all_letters[top_i]
output_str += char
chars_input = make_chars_input(char)
return output_str
# 再定义一个函数,方便每次生成多个名字
def generate(category, start_chars='ABC'):
for start_char in start_chars:
print(generate_one(category, start_char))
generate('Russian', 'RUS')
Raine
Urlamov
Sakhgany
generate('German', 'GER')
Gros
Echer
Rober
generate('Spanish', 'SPA')
Salvan
Pare
Alen
看起来还不错哈!不过这个模型还是有点简单,我们以后还可以对它进行改进,发明出更多的玩法!
更多玩法和改进
本文中给出的是通过指定“国家名”生成“人名”,我们还可以通过改变训练数据集,搞出其它的玩法,比如:
通过小说 生成 小说中人物名字
通过演讲 生成 演讲词汇
通过国家 生成 城市名
如果要考虑到改进:
可以尝试增加神经网络的规模,来提升预测的效果
尝试把中文字符加进来,让神经网络可以生成中文的名字!
如果要使用中文,注意要控制输入层的规模,可以试试嵌入
PyTorch圣殿 | 传奇NLP攻城狮成长之路
课程表
本期:起名大师:使用RNN生成个好名字
第五期:AI翻译官:采用注意力机制的翻译系统
第六期:探索词向量世界
第七期:词向量高级:单词语义编码器
第八期:长短记忆神经网络(LSTM)序列建模
第九期:体验PyTorch动态编程,双向LSTM+CRF
推荐阅读
AI 有嘻哈 | 使用 PyTorch 搭建一个会嘻哈的深度学习模型
教程 | Windows用户指南:如何用Floyd跑PyTorch
学员原创 | 人工智能产品经理的新起点(200页PPT下载)
吐血推荐:超级好用的深度学习云平台Floyd | 集智AI学园
关注集智AI学园公众号
获取更多更有趣的AI教程吧!
搜索微信公众号:swarmAI
集智AI学园QQ群:426390994
学园网站:campus.swarma.org
商务合作|zhangqian@swarma.org
投稿转载|wangjiannan@swarma.org